-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local utils = require 'mc.utils'
local definition = require 'canbus.definition'
local canbus_collection = require 'canbus.collection'
local canbus_dispatcher = require 'canbus.dispatcher'
local log = require 'mc.logging'
local skynet = require 'skynet'
local ctx = require 'mc.context'
local json = require 'cjson'

local remote_access = {}

remote_access.response_property = {}
remote_access.response_method = nil

function remote_access.register(bus)
    local canbus_co = canbus_collection.get_instance()
    local canbus_enabled = canbus_co.canbus_enabled
    if not canbus_enabled then
        log:notice('canbus is disabled and cannot register remote call')
        return
    end

    -- register callbacks to dispatcher
    canbus_dispatcher.get_instance():register_cmd_handler(definition.can_cmds.REMOTE_GET_PROPERTY_REQ,
        remote_access.get_prop_recv)
    canbus_dispatcher.get_instance():register_cmd_handler(definition.can_cmds.REMOTE_GET_PROPERTY_RSP,
        remote_access.get_prop_return_recv)
    canbus_dispatcher.get_instance():register_cmd_handler(definition.can_cmds.REMOTE_CALL_REQ,
        remote_access.call_recv)
    canbus_dispatcher.get_instance():register_cmd_handler(definition.can_cmds.REMOTE_CALL_RSP,
        remote_access.call_retrun_recv)

    -- initialize the remote call dbus handler
    remote_access.bus = bus
end

function remote_access.get_prop_recv(can_id, channel, msgs)
    -- 判断本机是否是目标地址
    if can_id.dest_id ~= canbus_collection.get_instance().identifier then
        return
    end

    -- message will be split by semicolon (;)
    local items = utils.split(table.concat(msgs):sub(3):match('^%s*(.-)%s*$'), ';')  -- 前2个字节分别是cmd和（;）所以从第3个字节开始
    local args, head, tail = {}, nil, nil
    for _, v in ipairs(items) do
        for _, key in pairs(definition.remote_access_keys) do
            head, tail = v:find(key)
            if head and tail then
                args[key] = v:sub(tail + 1)
            end
        end
    end

    -- properties will be split again by comma (,)
    local properties = args[definition.remote_access_keys.PROPERTIES]
    if properties then
        args[definition.remote_access_keys.PROPERTIES] = utils.split(properties, ',')
    end

    -- get properties according to arguments (calling sdbus interface with no context)
    local sdbus_args = {
        args[definition.remote_access_keys.SERVICE],
        args[definition.remote_access_keys.PATH],
        'bmc.kepler.Object.Properties',
        'GetAllWithContext',
        'a{ss}s',
        ctx.get_context_or_default(),
        args[definition.remote_access_keys.INTERFACE]
    }
    local is_err, res = remote_access.bus:pcall(table.unpack(sdbus_args))
    if is_err then
        log:error('get properties [service: %s] error: %s', args[definition.remote_access_keys.SERVICE], res)
        return false
    end

    -- package the message and send to source [double channel send]
    properties = {}
    for _, v in pairs(args[definition.remote_access_keys.PROPERTIES]) do
        if res[v] then
            table.insert(properties, v .. ':' .. res[v]:value())
        else
            log:notice('property [%s] cannot be obtained', v)
        end
    end

    canbus_collection.get_instance():send(table.concat({
        string.char(definition.can_cmds.REMOTE_GET_PROPERTY_RSP), -- 1st byte of 1st frame is cmd
        table.concat(properties, ',')
    }, ''), tonumber(channel), can_id.src_id)
end

function remote_access.get_prop_return_recv(can_id, channel, msgs)
    -- 判断本机是否是目标地址
    if can_id.dest_id ~= canbus_collection.get_instance().identifier then
        return
    end
    -- 数据格式k:v,k:v
    local properties = table.concat(msgs):sub(2):match('^%s*(.-)%s*$') -- 第2个字节开始是属性值，第1个字节是cmd
    local key, value
    local result = {}
    for pair in string.gmatch(properties, '([^,]+)') do
        key, value = pair:match('([^:]+):?(.*)')
        if key then
            result[key] = value ~= '' and value or nil
        end
    end
    remote_access.response_property = result
end

-- 将json串的kv结构转换成table的同时转换数据类型
-- 当前先仅作简单判断，认为步时字符串就是数字，还需要考虑复杂结构
local function format_method_args(sigs, arguments, args)
    if sigs == 'a{ss}' then
        return
    end

    local arguments_t = json.decode(arguments)
    local temp = {}  -- 记录每个key的下标，和签名做比较，因为无序列表无法保证遍历的顺序
    for key in arguments:gmatch('"([^"]+)"%s*:%s*') do
        table.insert(temp, key)
    end

    local suffix = sigs:sub(6)  -- 前5个字节是a{ss}所以从6开始
    for i = 1, #suffix do
        if string.sub(suffix, i, i) ~= 's' then
            arguments_t[temp[i]] = tonumber(arguments_t[temp[i]])
        end
    end

    local res = {}  -- 表中仅存放value
    for _, prop in pairs(temp) do
        table.insert(res, arguments_t[prop])
    end

    args[definition.remote_access_keys.ARGUMENTS] = res
end

function remote_access.call_recv(can_id, channel, msgs)
    -- 判断本机是否是目标地址
    if can_id.dest_id ~= canbus_collection.get_instance().identifier then
        return
    end
    -- message will be split by semicolon (;)
    local items = utils.split(table.concat(msgs):sub(3):match('^%s*(.-)%s*$'), ';')  -- 第3个字节开始才是数据
    local args, head, tail = {}, nil, nil
    for _, v in ipairs(items) do
        for _, key in pairs(definition.remote_access_keys) do
            head, tail = v:find(key)
            if head and tail then
                args[key] = v:sub(tail + 1)
            end
        end
    end

    -- properties will be split again by comma (,)
    local arguments = args[definition.remote_access_keys.ARGUMENTS]
    if arguments then
        format_method_args(args[definition.remote_access_keys.SIGNATURE], arguments, args)
    end

    -- remote call according to arguments (calling sdbus interface with no context)
    local sdbus_args = {
        args[definition.remote_access_keys.SERVICE],
        args[definition.remote_access_keys.PATH],
        args[definition.remote_access_keys.INTERFACE],
        args[definition.remote_access_keys.METHOD],
        args[definition.remote_access_keys.SIGNATURE],
        ctx.get_context_or_default()
    }

    -- 插入列表中才能够执行unpack
    for _, value in pairs(args[definition.remote_access_keys.ARGUMENTS]) do
        table.insert(sdbus_args, value)
    end

    local is_err, res = remote_access.bus:pcall(table.unpack(sdbus_args))
    if is_err then
        log:error('get remote method [service: %s] error: %s', args[definition.remote_access_keys.SERVICE], res)
        return false
    end

    local return_msg = ''
    if res then
        if type(res) == 'table' then
            local ok, msg = pcall(json.encode, res)
            return_msg = ok and msg or ''
        else
            return_msg = tostring(res) or ''
        end
    end
    canbus_collection.get_instance():send(table.concat({
        string.char(definition.can_cmds.REMOTE_CALL_RSP), -- 1st byte of 1st frame is cmd
        return_msg
    }, ''), tonumber(channel), can_id.src_id)
end

function remote_access.call_retrun_recv(can_id, channel, msgs)
    -- 判断本机是否是目标地址
    if can_id.dest_id ~= canbus_collection.get_instance().identifier then
        return
    end

    remote_access.response_method = table.concat(msgs):sub(2):match('^%s*(.-)%s*$')  -- 第一个字节是cmd，所以从2开始
end

-- remote get properties using synchronized and blocked way
function remote_access.get_prop(channel, destination, service, path, interface, properties)
    -- validate the arguments
    if not channel or not canbus_collection.get_instance().collections[channel] then
        return definition.remote_access_errors.INVALID_CHANNEL
    end
    if not (destination and (destination > 0 and destination < definition.dest.BOARDCAST)) then
        return definition.remote_access_errors.INVALID_DESTINATION
    end

    -- check current remote call is available
    if remote_access.is_busy then
        return definition.remote_access_errors.IS_BUSY
    end

    -- makeup the canbus message payload
    -- arguments using semicolon(;)
    -- properties will be combined using comma(,)
    remote_access.is_busy = true
    canbus_collection.get_instance():send(table.concat({
        string.char(definition.can_cmds.REMOTE_GET_PROPERTY_REQ), -- 1st byte of 1st frame is cmd
        definition.remote_access_keys.SERVICE .. service,
        definition.remote_access_keys.PATH .. path,
        definition.remote_access_keys.INTERFACE .. interface,
        definition.remote_access_keys.PROPERTIES .. table.concat(properties, ',')
    }, ';'), channel, destination)

    -- wait response with timeout
    for i = 1, definition.remote_access_threshold.GET_PROPERTY // definition.remote_access_threshold.STEP do
        if next(remote_access.response_property) then
            local response = remote_access.response_property
            remote_access.response_property = {}
            remote_access.is_busy = false
            return definition.remote_access_errors.OK, response
        end

        skynet.sleep(definition.remote_access_threshold.STEP // 10) -- step change to skynet sleep ticks
    end
    log:error('remote get properties is timeout [threshold: %sms]', definition.remote_access_threshold.GET_PROPERTY)
    remote_access.is_busy = false
    return definition.remote_access_errors.TIMEOUT
end

-- remote call method using synchronized and blocked way
-- NOTE: need sequence if the asynchronous situation occurs
function remote_access.call(channel, destination, service, path, interface, method, signature, args)
    -- validate the arguments
    if not channel or not canbus_collection.get_instance().collections[channel] then
        return definition.remote_access_errors.INVALID_CHANNEL
    end
    if not (destination and (destination > 0 and destination < definition.dest.BOARDCAST)) then
        return definition.remote_access_errors.INVALID_DESTINATION
    end

    -- check current remote call is available
    if remote_access.is_busy then
        return definition.remote_access_errors.IS_BUSY
    end

    -- makeup the canbus message payload with all arguments using semicolon(;)
    remote_access.is_busy = true
    canbus_collection.get_instance():send(table.concat({
        string.char(definition.can_cmds.REMOTE_CALL_REQ), -- 1st byte of 1st frame is cmd
        definition.remote_access_keys.SERVICE .. service,
        definition.remote_access_keys.PATH .. path,
        definition.remote_access_keys.INTERFACE .. interface,
        definition.remote_access_keys.METHOD .. method,
        definition.remote_access_keys.SIGNATURE .. signature,
        definition.remote_access_keys.ARGUMENTS .. json.encode(args)
    }, ';'), channel, destination)

    -- wait response with timeout
    for i = 1, definition.remote_access_threshold.CALL // definition.remote_access_threshold.STEP do
        if remote_access.response_method then
            local response = remote_access.response_method
            remote_access.response_method = nil
            remote_access.is_busy = false
            return definition.remote_access_errors.OK, response
        end

        skynet.sleep(definition.remote_access_threshold.STEP // 10) -- step change to skynet sleep ticks
    end
    log:error('remote call is timeout [threshold: %sms]', definition.remote_access_threshold.CALL)
    remote_access.is_busy = false
    return definition.remote_access_errors.TIMEOUT
end

return remote_access